import datetime
import numpy as np

import os
import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
from matplotlib import pyplot as plt
from torchvision.models import DenseNet121_Weights
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfbase import pdfmetrics
from reportlab.lib import colors
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
from reportlab.platypus import Image  as imagesss
from datetime import datetime
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image as ReportLabImage
from reportlab.platypus import ListFlowable, ListItem
from reportlab.lib.styles import ParagraphStyle
from reportlab.platypus import ListItem, Paragraph, Spacer
from PIL import Image  # 导入Pillow库中的Image类

pdfmetrics.registerFont(TTFont('MicrosoftYaHei', 'C:/Windows/Fonts/msyh.ttc'))

CKPT_PATH = 'C:\\Users\\Administrator\\Desktop\\land\\src\\BackEnd\\chexNet\\new_model_checkpoint.pth'
root_path = 'C:\\Users\\Administrator\\Desktop\\land\\src\\BackEnd\\uploads'
txt_file_path = 'config.txt'  # 请替换为实际的 txt 文件路径
N_CLASSES = 14

normalize = transforms.Normalize([0.485, 0.456, 0.406],
                                 [0.229, 0.224, 0.225])

class_names = [ '肺不张','心脏肥大','积液','浸润型肺结核','肿块','结节','肺炎','气胸','变实','水肿','肺气肿','纤维变性','胸膜增厚','疝气']

THRESHOLDS = [0.8, 0.9, 0.9, 0.8, 0.9, 0.9, 0.85, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9]
thresholds = {
    "肺不张": 0.8, "变实": 0.9, "浸润型肺结核": 0.9, "气胸": 0.8, "水肿": 0.9,
    "肺气肿": 0.9, "纤维变性": 0.85, "积液": 0.9, "肺炎": 0.9, "胸膜增厚": 0.9,
    "心脏肥大": 0.9, "结节": 0.9, "肿块": 0.9, "疝气": 0.9,
}

# 对应的检测结果文本
disease_results = {
    "肺不张":    "模型检测结果显示,您有较大可能患有肺不张,若您最近存在呼吸困难、发绀、胸痛、心慌等症状,建议您咨询医师进行进一步的检查。",
    "变实":     "模型检测结果显示 , 您有较大可能患有变实症状,若您最近存在咳嗽、发热、胸痛、呼吸急促、疲劳和体力不足等症状,建议您咨询医师进行进一步的检查和治疗。",
    "浸润型肺结核": "模型检测结果显示,您有较大可能患有浸润型肺结核,若您最近存在咳嗽、咳痰和咯血等症状,建议您咨询医师进行进一步的检查。",
    "气胸":     "模型检测结果显示,您有较大可能患有气胸 若您最近存在刺激性咳嗽,心律失常、呼吸快速、皮肤发紫、疲乏无力等症状,建议您咨询医师进行进一步的检查。",
    "水肿":     "模型检测结果显示,您有较大可能患有水肿 若您最近存在下肢,食欲不振,气短等症状,建议您咨询医师进行进一步的检查。",
    "肺气肿":    "模型检测结果显示,您有较大可能患有肺气肿 若您最近存在劳力性呼吸急促,伴随咳嗽、咳痰等症状,逐渐加重的呼吸困难、胸闷、气短。建议您咨询医师进行进一步的检查。",
    "纤维变性":   "模型检测结果显示,您有较大可能患有纤维变性 若您最近存在干咳、进行性呼吸困难、头晕,口唇、指甲发紫,活动受限等缺氧表现,建议您咨询医师进行进一步的检查。",
    "积液":     "模型检测结果显示,您有较大可能患有积液 若您最近存在心悸、呼吸急促、呼吸困难等症状,建议您咨询医师进行进一步的检查。",
    "肺炎":     "模型检测结果显示,您有较大可能患有肺炎 若您最近存在咳嗽、咳痰,伴有呼吸急促、呼吸困难、伴有发热、咳嗽、气促、肺部固定啰音等症状,建议您咨询医师进行进一步的检查。",
    "胸膜增厚":   "模型检测结果显示,您有较大可能患有胸膜增厚 若您最近存在呼吸困难、胸痛、持续性咳嗽、疲劳感、活动耐力下降等症状,建议您咨询医师进行进一步的检查。",
    "心脏肥大":   "模型检测结果显示,您有较大可能患有心脏肥大 若您最近存在呼吸急促、疲劳、胸痛、心悸、腿部肿胀或水肿、心跳不规律等症状,建议您咨询医师进行进一步的检查。",
    "结节":     "模型检测结果显示,您有较大可能患有结节 若您最近存在持续的咳嗽、胸痛、气短、咳痰中带血等症状,建议您咨询医师进行进一步的检查。",
    "肿块":     "模型检测结果显示,您有较大可能患有肿块 若您最近存在咳嗽、胸痛、呼吸困难、持续性咳痰或咳痰带血等症状,建议您咨询医师进行进一步的检查。",
    "疝气":     "模型检测结果显示,您有较大可能患有疝气 若您最近存在局部肿块、胸部不适或疼痛等症状,建议您咨询医师进行进一步的检查。",
}

health_advice_dict = {
    "肺不张": [
        ['生活方式', '建议保持健康饮食、增强体力活动、戒烟戒酒'],
        ['疾病预防', '提高呼吸道卫生，避免接触病毒，及时接种流感疫苗'],
        ['就医建议', '建议进行进一步的CT检查以确认病变程度，必要时进行抗生素治疗'],
        ['定期复查', '建议每3个月进行一次X光检查，观察病变是否有变化']
    ],
    "变实": [
        ['生活方式', '建议保持健康饮食、增强体力活动、戒烟戒酒'],
        ['疾病预防', '提高呼吸道卫生，避免接触病毒，及时接种流感疫苗'],
        ['就医建议', '建议进行进一步的CT检查以确认病变程度，必要时进行抗生素治疗'],
        ['定期复查', '建议每3个月进行一次X光检查，观察病变是否有变化']
    ],
    "浸润型肺结核": [
        ['生活方式', '保持良好的呼吸道卫生，避免与结核患者接触'],
        ['疾病预防', '及时接种结核疫苗，保持空气流通'],
        ['就医建议', '建议进行痰液检查及胸部CT，以确认病变类型，必要时进行抗结核治疗'],
        ['定期复查', '建议定期复查胸部影像学检查，以观察病变进展']
    ],
    "气胸": [
        ['生活方式', '避免剧烈运动及体力负担过重，避免高海拔环境'],
        ['疾病预防', '加强肺部健康，定期进行胸部X光检查'],
        ['就医建议', '建议进行胸部CT检查，确认气胸的类型和范围，必要时进行穿刺或胸腔引流'],
        ['定期复查', '建议定期复查胸部X光，观察是否有复发']
    ],
    "水肿": [
        ['生活方式', '保持均衡饮食，减少盐分摄入，增强体力活动'],
        ['疾病预防', '避免长时间站立或坐着，定期运动'],
        ['就医建议', '建议进行心脏超声检查，评估是否伴随有心脏病，必要时进行利尿治疗'],
        ['定期复查', '建议定期进行血压监测，观察水肿症状是否有变化']
    ],
    "肺气肿": [
        ['生活方式', '戒烟，避免接触有害气体，加强体力活动'],
        ['疾病预防', '定期接种流感疫苗，避免呼吸道感染'],
        ['就医建议', '建议进行肺功能测试及胸部CT检查，必要时使用支气管扩张剂和氧疗'],
        ['定期复查', '建议每年进行一次肺功能检查和胸部影像学检查']
    ],
    "纤维变性": [
        ['生活方式', '避免剧烈运动，保持良好的姿势和呼吸习惯'],
        ['疾病预防', '避免接触有害物质，保持室内空气流通'],
        ['就医建议', '建议进行肺部功能测试及CT扫描，必要时进行药物治疗以减缓病程'],
        ['定期复查', '建议每半年复查一次肺功能和胸部影像学检查']
    ],
    "积液": [
        ['生活方式', '保持适当的运动量，避免长时间卧床'],
        ['疾病预防', '保持良好的呼吸道卫生，避免感染'],
        ['就医建议', '建议进行胸部CT检查，评估积液的性质和范围，必要时进行胸腔引流'],
        ['定期复查', '建议定期进行胸部影像学检查，观察病情变化']
    ],
    "肺炎": [
        ['生活方式', '保持健康饮食，增加体力活动，戒烟戒酒'],
        ['疾病预防', '定期接种流感疫苗，避免感冒和其他呼吸道感染'],
        ['就医建议', '建议进行胸部CT检查和痰液检查，必要时使用抗生素治疗'],
        ['定期复查', '建议每3个月进行一次X光检查，观察肺炎恢复情况']
    ],
    "胸膜增厚": [
        ['生活方式', '避免过度疲劳和剧烈运动'],
        ['疾病预防', '保持呼吸道健康，避免接触有害物质'],
        ['就医建议', '建议进行胸部CT检查，确认胸膜增厚程度，必要时进行药物治疗'],
        ['定期复查', '建议每6个月进行一次X光检查，观察病情进展']
    ],
    "心脏肥大": [
        ['生活方式', '保持健康饮食，减少盐分摄入，增加体力活动'],
        ['疾病预防', '定期进行心电图检查，监测血压'],
        ['就医建议', '建议进行心脏超声检查，评估心脏功能，必要时使用药物治疗'],
        ['定期复查', '建议定期进行心脏检查，监测心脏功能变化']
    ],
    "结节": [
        ['生活方式', '保持健康饮食，避免接触有害物质'],
        ['疾病预防', '定期进行胸部影像学检查，关注健康状况'],
        ['就医建议', '建议进行CT扫描以确认结节性质，必要时进行活检'],
        ['定期复查', '建议每6个月进行一次胸部X光检查，观察结节的变化']
    ],
    "肿块": [
        ['生活方式', '保持健康饮食，避免接触有害物质'],
        ['疾病预防', '定期进行胸部影像学检查，关注健康状况'],
        ['就医建议', '建议进行胸部CT检查及活检，确认肿块的性质，必要时进行手术或化疗'],
        ['定期复查', '建议每6个月进行一次胸部影像学检查，监测肿块的变化']
    ],
    "疝气": [
        ['生活方式', '避免剧烈运动，保持良好的姿势'],
        ['疾病预防', '定期进行体检，避免肥胖'],
        ['就医建议', '建议进行体格检查，必要时进行影像学检查，可能需要手术治疗'],
        ['定期复查', '建议术后定期复查，以确保恢复良好']
    ]
}

# 生成每个病症的健康建议表格
def generate_health_advice_table(disease, health_advice_list, heatmap_path):
    # 初始化表格数据，第一行是表头
    data = [['项目', '建议/措施']]  # 表头只有两列，项目和建议/措施
    # 添加一行专门展示热力图
    data.append(['热力图', ReportLabImage(heatmap_path, width=80, height=80)])
    # 将每个健康建议添加到表格中
    for advice in health_advice_list:
        data.append([advice[0], advice[1]])  # 每个健康建议显示在表格的一行中
    # 设置表格样式
    table_style = TableStyle([
        ('BACKGROUND', (0, 0), (-1, 0), colors.grey),  # 表头背景色
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),  # 表头字体颜色
        ('ALIGN', (0, 0), (-1, -1), 'CENTER'),  # 所有单元格内容居中对齐
        ('FONTNAME', (0, 0), (-1, -1), 'MicrosoftYaHei'),  # 字体设置
        ('BOTTOMPADDING', (0, 0), (-1, 0), 12),  # 表头底部的空白区域
        ('BACKGROUND', (0, 1), (-1, -1), colors.beige),  # 数据行背景色
        ('GRID', (0, 0), (-1, -1), 1, colors.black),  # 表格网格线
    ])

    # 创建表格对象
    table = Table(data)
    table.setStyle(table_style)

    return table
# 对应的健康建议
health_advices = {
    "肺不张": "建议避免剧烈运动和重体力劳动，保持良好的作息和饮食习惯，避免吸烟和有害气体的暴露。定期进行肺功能检查，遵循医生的复查安排，避免压力过大。",
    "变实": "建议避免长时间保持同一姿势，尤其是久坐，尽量避免过度劳累和过度紧张。保持规律作息，增加适当的锻炼，有助于维持健康的体态和改善呼吸功能。",
    "浸润型肺结核": "建议避免长期熬夜，保持充足的睡眠，增强体质，避免感冒等呼吸道疾病的诱发。坚持积极治疗，并定期复查，注意保持良好的呼吸道卫生。",
    "气胸": "建议避免剧烈运动和负重活动，避免咳嗽过于剧烈。保持规律的作息，增强体质，定期检查肺部健康，避免高风险活动。",
    "水肿": "建议减少盐分摄入，避免长时间站立或久坐，适当增加运动量以促进血液循环，保持健康的体重，保持水分平衡。定期检查心肺功能。",
    "肺气肿": "建议戒烟，避免吸入有害气体，保持规律的运动和健康饮食，避免过度劳累。定期进行肺功能检查，保持适当的体重，避免呼吸道感染。",
    "纤维变性": "建议避免剧烈运动和过度劳累，保持规律的作息和健康饮食。进行适当的呼吸训练和肺部康复，增强肺部的功能，定期复查肺部健康。",
    "积液": "建议避免剧烈活动和过度劳累，保持合适的体重，避免长期站立或久坐。定期进行心肺功能检查，合理安排作息和饮食，确保足够的休息时间。",
    "肺炎": "建议保持良好的个人卫生习惯，避免呼吸道感染，避免接触病菌源，保持充足的睡眠和良好的饮食习惯。避免过度劳累，增强身体免疫力。",
    "胸膜增厚": "建议避免暴露于有害气体、粉尘或污染环境，保持室内空气清新。保持规律的作息，定期检查肺部健康，避免过度劳累和呼吸道感染。",
    "心脏肥大": "建议保持健康饮食，避免过量摄入盐分和高脂肪食物，减少压力，定期进行心脏检查。保持适当的运动量，有助于心脏健康，避免剧烈运动。",
    "结节": "建议戒烟，避免接触有害物质，保持规律的体检，注意定期检查肺部和胸部健康。保持适当的体重，增强身体免疫力，避免过度劳累。",
    "肿块": "建议避免剧烈运动，避免提重物，避免过度劳累。保持健康的生活方式，定期进行检查，尤其是早期筛查，及时发现病变并治疗。",
    "疝气": "建议避免提重物和剧烈活动，避免增加腹部压力，保持健康饮食，控制体重，避免便秘。定期复查，注意腹部健康，保持良好的生活作息。",
}

def overlay_heatmap_on_image_per_class(heatmap, image_path, save_path, class_names, alpha=0.5):
    """
    将每个类别的热力图单独叠加到原始图像上，并保存每个类别的结果。

    Parameters:
    - heatmap: 形状为 (num_classes, H', W') 的张量，包含多个类别的热力图
    - image_path: 原始图像的路径
    - save_path: 保存叠加图像的路径
    - class_names: 类别名称列表
    - alpha: 热力图的透明度，默认为0.5
    """
    # 加载原始图像并调整为与热力图大小一致
    original_image = Image.open(image_path).convert('RGB')
    original_image = np.array(original_image)

    # 创建保存文件夹（如果不存在）
    os.makedirs(save_path, exist_ok=True)

    # 遍历每个类别的热力图并进行叠加
    for i, class_name in enumerate(class_names):
        # 获取当前类别的热力图 (H', W')
        class_heatmap = heatmap[i]

        # 归一化热力图到0-1范围
        class_heatmap -= class_heatmap.min()  # 将热力图最小值设为0
        class_heatmap /= class_heatmap.max()  # 将热力图最大值设为1

        # 将热力图转换为伪彩色（jet）
        heatmap_rgb = plt.cm.jet(class_heatmap)[:, :, :3]  # 使用jet颜色映射，取前三个通道（RGB）

        # 调整热力图与原始图像的大小一致
        heatmap_rgb_resized = np.array(Image.fromarray((heatmap_rgb * 255).astype(np.uint8)).resize((original_image.shape[1], original_image.shape[0])))

        # 叠加热力图到原始图像
        overlay_image = (original_image * (1 - alpha) + heatmap_rgb_resized * alpha).astype(np.uint8)

        # 保存叠加图像
        overlay_image_path = os.path.join(save_path, f"{class_name}_heatmap_overlay.png")
        Image.fromarray(overlay_image).save(overlay_image_path)

        print(f"{class_name} 类别的叠加图像已保存到 {overlay_image_path}")

    return save_path

# 修改后的生成热力图的函数，返回每个病症单独的热力图
def generate_individual_heatmap(heatmap, output_prob, class_names, disease, save_path,img_path):
    """
    根据输出概率和阈值,生成单独的热图，并保存每个病症的热力图
    """
    heatmap = heatmap.squeeze(0).cpu().numpy()  # 移除批次维度并转换为 numpy
    combined_heatmap = np.zeros_like(heatmap[0], dtype=np.float32)  # 初始化叠加热图

    disease_index = class_names.index(disease)  # 获取当前病症的索引

    if output_prob[disease_index] >= THRESHOLDS[disease_index]:
        # 对当前类别的热图进行归一化
        original_image = Image.open(img_path).convert('RGB')
        original_image = original_image.resize((heatmap.shape[1], heatmap.shape[2]))  # 与热图对齐
        original_image = np.array(original_image)  # 转为 numpy 数组格式
        class_heatmap = heatmap[disease_index].astype(np.float32)  # 将当前热图转换为浮点类型
        class_heatmap -= class_heatmap.min()  # 减去最小值
        class_heatmap /= class_heatmap.max()  # 归一化到 0-1 范围
        class_heatmap[class_heatmap < 0.85] = 0  # 只保留大于0.85的区域

        # 保存该病症的热力图
        heatmap_path = os.path.join(save_path, f"{disease}_heatmap.png")
        plt.imshow(original_image, cmap='gray')

        # 叠加热图
        plt.imshow(class_heatmap, cmap='jet', alpha=0.4)
        plt.axis('off')
        plt.savefig(heatmap_path, bbox_inches='tight', pad_inches=0)
        plt.close()

        return heatmap_path
    return None


def get_disease_result(probability, disease):
    thresholds = {
        "肺不张": 0.8, "变实": 0.9, "浸润型肺结核": 0.9, "气胸": 0.8, "水肿": 0.9,
        "肺气肿": 0.9, "纤维变性": 0.85, "积液": 0.9, "肺炎": 0.9, "胸膜增厚": 0.9,
        "心脏肥大": 0.9, "结节": 0.9, "肿块": 0.9, "疝气": 0.9,
    }

    threshold = thresholds.get(disease, 0.5)  # 默认阈值为0.5

    if probability >= threshold:
        return disease_results[disease]
    else:
        return None

# 提供健康建议文本
def get_health_advice(probability, disease):
    thresholds = {
        "肺不张": 0.8, "变实": 0.9, "浸润型肺结核": 0.9, "气胸": 0.8, "水肿": 0.9,
        "肺气肿": 0.9, "纤维变性": 0.85, "积液": 0.9, "肺炎": 0.9, "胸膜增厚": 0.9,
        "心脏肥大": 0.9, "结节": 0.9, "肿块": 0.9, "疝气": 0.9,
    }

    threshold = thresholds.get(disease, 0.5)  # 默认阈值为0.5

    if probability >= threshold:
        return health_advices[disease]
    else:
        return f"针对{disease}的健康建议：保持健康的生活方式,定期进行体检,保持积极的心态。"
def generate_pdf_report(result, user_id, heatmap, class_names, img_path):
    title_style = ParagraphStyle(
        name='Title',
        fontName='MicrosoftYaHei',
        fontSize=24,
        alignment=1  # 1 表示居中对齐
    )

    # 定义正文样式（左对齐）
    body_style = ParagraphStyle(
        name='Body',
        fontName='MicrosoftYaHei',
        fontSize=12,
        alignment=1  # 0 表示左对齐
    )
    probabilities = result  # 从预测结果中获取概率值

    # 获取用户路径
    user_path = f"C:/Users/Administrator/Desktop/land/src/BackEnd/uploads/{user_id}/result"
    img_dir = f"C:/Users/Administrator/Desktop/land/src/BackEnd/uploads/{user_id}"

    # 获取当前时间，精确到分钟
    current_time = datetime.now().strftime("%Y-%m-%d_%H-%M")

    # 设置PDF文件保存路径
    pdf_file_path = os.path.join(user_path, f"{current_time}.pdf")

    # 创建用户的 result 文件夹（如果目录不存在）
    if not os.path.exists(user_path):
        os.makedirs(user_path)

    # 创建PDF文档
    doc = SimpleDocTemplate(pdf_file_path, pagesize=A4)

    # 设置内容列表
    story = []

    # 1. 胸片 X 光健康监测报告概述
    story.append(Paragraph("<center><b>胸片 X 光健康监测报告</b></center>", style=title_style))
    story.append(Spacer(1, 30))

    # 用户信息部分
    story.append(Paragraph(f"用户ID: {user_id}", style=body_style))
    story.append(Paragraph("用户名: zhangsan", style=body_style))
    story.append(Spacer(1, 20))

    # 2. 检测结果部分
    story.append(Paragraph("<b>病例检测结果：</b>", style=ParagraphStyle(name='Normal', fontName='MicrosoftYaHei', fontSize=16)))
    story.append(Spacer(1, 10))

    # 创建表格数据
    data = [['病症名称', '检测结果']]
    positive_cases = []  # 用来保存阳性病例的分析
    for i, disease in enumerate(class_names):
        # 判断是否为阳性或阴性
        result_status = "阳性" if probabilities[i] >= THRESHOLDS[i] else "阴性"
        disease_with_probability = f"{disease} ({probabilities[i]*100:.2f}%)"
        data.append([disease_with_probability, result_status])

        # 如果是阳性，收集分析文本
        if probabilities[i] >= THRESHOLDS[i]:  # 只有当概率大于阈值时，才插入健康建议
            analysis = get_disease_result(probabilities[i], disease)
            positive_cases.append(f"{disease}：{analysis}")

    # 设置列宽，固定列宽
    col_widths = [200, 100]  # 设置两列的宽度，第一列宽200，第二列宽100

    # 创建表格并设置列宽
    table = Table(data, colWidths=col_widths)

    # 设置表格样式
    table.setStyle(TableStyle([
        ('BACKGROUND', (0, 0), (-1, 0), colors.grey),  # 表头背景色
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),  # 表头字体颜色
        ('ALIGN', (0, 0), (-1, -1), 'CENTER'),  # 所有单元格内容居中对齐
        ('FONTNAME', (0, 0), (-1, -1), 'MicrosoftYaHei'),  # 字体设置
        ('BOTTOMPADDING', (0, 0), (-1, 0), 12),  # 表头底部的空白区域
        ('BACKGROUND', (0, 1), (-1, -1), colors.beige),  # 数据行背景色
        ('GRID', (0, 0), (-1, -1), 1, colors.black),  # 表格网格线
        ('WORDWRAP', (0, 0), (-1, -1), 'C'),  # 启用单元格换行
        ('MAXIMUM', (0, 0), (-1, -1), 1),  # 设置单元格的最大宽度
    ]))

    # 将表格添加到故事中
    story.append(table)
    story.append(Spacer(1, 20))

    # 3. 结果分析部分
    story.append(Paragraph("<b>结果分析</b>", style=ParagraphStyle(name='Normal', fontName='MicrosoftYaHei', fontSize=18)))
    story.append(Spacer(1, 10))

    # 计算阳性和阴性病例数量
    positive_count = sum([1 for i in range(len(probabilities)) if probabilities[i] >= THRESHOLDS[i]])  # 阈值为0.5
    negative_count = len(probabilities) - positive_count
    story.append(Paragraph(f"根据模型结果显示，总共检测到 {positive_count} 个阳性病例, {negative_count} 个阴性病例。", style=ParagraphStyle(name='Normal', fontName='MicrosoftYaHei', fontSize=12)))

    # 如果有阳性病例，列出每个阳性病例的详细分析
    if positive_cases:
        story.append(Spacer(1, 5))  # 为列表前添加空白空间
        list_items = [
            ListItem(
                Paragraph(f" {case}", style=ParagraphStyle(name='Normal', fontName='MicrosoftYaHei', fontSize=12, leading=14))  # 设置每行的行距
            )
            for case in positive_cases
        ]
    # 使用ListFlowable创建带有黑点的列表，调整列表项之间的行间距
    list_flowable = ListFlowable(list_items, bulletType='bullet', spaceAfter=12, spaceBefore=12)
    story.append(list_flowable)
    story.append(Spacer(1, 10))  # 为列表后添加空白空间

    story.append(Paragraph(f"以下为模型在各类阳性病例中重点注意的区域，通过类激活图(CAM)生成的热力图，您可以直观地看到病例对应的病变部位。", style=ParagraphStyle(name='Normal', fontName='MicrosoftYaHei', fontSize=12)))
    story.append(Spacer(1, 10))  # 为列表后添加空白空间
    for i, disease in enumerate(class_names):
        if probabilities[i] >= THRESHOLDS[i]:  # 只有当概率高于阈值时才插入热力图和建议
            # 生成每个病症的热力图
            heatmap_path = generate_individual_heatmap(heatmap, probabilities, class_names, disease, img_dir, img_path)
            if heatmap_path:
                # 插入病症的标题（作为段落代替列表）
                title_paragraph = Paragraph(f"<b>{disease} 病变部位及建议</b>", style=ParagraphStyle(name='Normal', fontName='MicrosoftYaHei', fontSize=14))
                story.append(title_paragraph)
                story.append(Spacer(1, 10))

                # 获取当前病症的健康建议
                health_advice_list = health_advice_dict.get(disease, [])

                # 如果该病症有健康建议，则生成并插入表格
                if health_advice_list:
                    health_advice_table = generate_health_advice_table(disease, health_advice_list, heatmap_path)
                    story.append(health_advice_table)
                    story.append(Spacer(1, 20))

    # 生成PDF
    doc.build(story)
    print(f"PDF报告已保存至: {pdf_file_path}")
    return pdf_file_path

def load_model():
    model = DenseNet121(N_CLASSES)
    checkpoint = torch.load(CKPT_PATH, map_location='cpu')  # 加载权重文件到 CPU
    state_dict = checkpoint['state_dict']

    # 去除 'module.' 前缀
    new_state_dict = {}
    for k, v in state_dict.items():
        new_key = k.replace("module.", "")  # 移除 'module.' 前缀
        new_state_dict[new_key] = v

    model.load_state_dict(new_state_dict)  # 加载处理后的权重
    return model

def save_combined_heatmap(heatmap, output_prob, image_path, save_path, class_names):
    """
    根据输出概率和阈值,生成叠加的热图,并只保留归一化后大于 0.85 的区域

    Parameters:
    - heatmap: Tensor, CAM 热图张量,形状为 (1, num_classes, H', W')
    - output_prob: Tensor, 每个类别的预测概率,形状为 (num_classes,)
    - image_path: str, 原始图像的路径
    - save_path: str, 保存综合热图的路径
    - class_names: list, 类别名称列表
    """
    heatmap = heatmap.squeeze(0).cpu().numpy()  # 移除批次维度并转换为 numpy
    original_image = Image.open(image_path).convert('RGB')
    original_image = original_image.resize((heatmap.shape[1], heatmap.shape[2]))  # 与热图对齐
    original_image = np.array(original_image)  # 转为 numpy 数组格式

    combined_heatmap = np.zeros_like(heatmap[0], dtype=np.float32)  # 初始化叠加热图

    for i, threshold in enumerate(THRESHOLDS):
        if output_prob[i] >= threshold:
            print(f"类别 '{class_names[i]}' 超过阈值 {threshold},加入热图叠加。")

            # 对当前类别的热图进行归一化
            class_heatmap = heatmap[i].astype(np.float32)  # 将当前热图转换为浮点类型
            class_heatmap -= class_heatmap.min()  # 减去最小值
            class_heatmap /= class_heatmap.max()  # 归一化到 0-1 范围

            # 只保留大于 0.85 的区域
            class_heatmap[class_heatmap < 0.85] = 0

            # 累加到综合热图
            combined_heatmap += class_heatmap

    # 对叠加后的热图进行裁剪,使值在 0 到 1 之间
    combined_heatmap = np.clip(combined_heatmap, 0, 1)

    # 显示原始图像
    plt.imshow(original_image, cmap='gray')

    # 叠加热图
    plt.imshow(combined_heatmap, cmap='jet', alpha=0.4)  # 透明度控制叠加效果
    plt.axis('off')
    plt.title("Combined Heatmap with Thresholded Regions")

    # 保存综合热图
    combined_heatmap_path = os.path.join(save_path, "combined_heatmap_thresholded.png")
    plt.savefig(combined_heatmap_path, bbox_inches='tight', pad_inches=0)
    plt.close()
    print(f"叠加热图已保存至 {combined_heatmap_path}")

def ten_crop_transform(crops):
    return torch.stack([transforms.ToTensor()(crop) for crop in crops])


def normalize_transform(crops):
    return torch.stack([normalize(crop) for crop in crops])


# 主函数
def main():
    # 读取 txt 文件
    txt_file=os.path.join(root_path,txt_file_path)
    with open(txt_file, 'r') as file:
        user_id = file.readline().strip()  # 第 1 行：用户路径
    image_name = 'image.png'

    # 构建图片路径
    image_path = os.path.join(root_path,user_id, image_name)

    # 检查图片是否存在
    if not os.path.exists(image_path):
        print(f"图片 {image_name} 不存在于路径 {user_id}")
        return
    user_path=os.path.join(root_path,user_id)
    # 读取图片并进行预处理
    preprocess = transforms.Compose([
        transforms.Resize(256),
        transforms.TenCrop(224),
        transforms.Lambda(ten_crop_transform),
        transforms.Lambda(normalize_transform)
    ])

    image = Image.open(image_path).convert('RGB')
    print(image_path)
    input_tensor = preprocess(image)
    temp=input_tensor.squeeze(0)
    temp=temp.numpy()
    n_crops, c, h, w = input_tensor.size()

    with torch.no_grad():
        input_var = input_tensor.view(-1, c, h, w)
    pred = torch.FloatTensor()

    # 加载模型并进行预测
    model = load_model()
    model = model.cpu()  # 确保模型在 CPU 上
    model.eval()
    output,heatmap = model(input_var)
    output_mean = output.view(n_crops, -1).mean(0)
    pred = torch.cat((pred, output_mean.data), 0)  # 加入批次维度

    # 保存模型输出
    output_save_path = os.path.join(root_path,user_id, "health_data.txt")

    # 将 tensor 转换为列表并格式化为字符串
    pred_str = " ".join([f"{p:.4f}" for p in pred.tolist()])  # 将每个元素格式化为 4 位小数
    if not os.path.exists(os.path.join(root_path,user_id)):
        os.makedirs(os.path.join(root_path,user_id))
    # 写入到文件
    with open(output_save_path, 'w') as f:
        f.write(pred_str)

    # 保存热图
    generate_pdf_report(pred,user_id,heatmap,class_names,image_path)


class DenseNet121(nn.Module):
    def __init__(self, out_size):
        super(DenseNet121, self).__init__()
        self.out_size = out_size
        self.densenet121 = torchvision.models.densenet121(pretrained=False)
        num_ftrs = self.densenet121.classifier.in_features
        self.densenet121.classifier = nn.Sequential(
            nn.Linear(num_ftrs, out_size),
            nn.Sigmoid()
        )

        # 添加前向钩子,用于捕获特征图
        self.densenet121.features[-1].register_forward_hook(self.hook_feature)
        self.feature = None

    def get_features(self, x):
        # 仅提取DenseNet的特征部分
        features = self.densenet121.features(x)
        return features

    def hook_feature(self, module, input, output):
        # 保存前向传播的特征图
        self.feature = output

    @staticmethod
    @torch.no_grad()
    def cam(feature_conv, weight_softmax, size):
        # 计算类激活图
        cams = torch.einsum('bchw, cn -> bnhw', feature_conv, weight_softmax.T)  # 生成 (10, 14, H', W')

        # 归一化处理
        # 首先在空间维度 (2, 3) 找到最小值
        cams_min = cams.min(dim=2, keepdim=True)[0]  # 在 H' 维度上找到最小值
        cams_min = cams_min.min(dim=2, keepdim=True)[0]  # 在 W' 维度上找到最小值

        cams = cams - cams_min  # 减去最小值

        # 找到最大值
        cams_max = cams.max(dim=2, keepdim=True)[0]  # 在 H' 维度上找到最大值
        cams_max = cams_max.max(dim=2, keepdim=True)[0]  # 在 W' 维度上找到最大值

        cams_img = cams / cams_max  # 归一化到 [0, 1]

        # 对每个类别的 CAM 进行平均,生成最终的 CAM
        cams_img_mean = cams_img.mean(dim=0, keepdim=True)  # (1, num_classes, H', W')

        # 插值调整回输入图像的大小
        cams_img_mean = nn.functional.interpolate(cams_img_mean, size=size, mode='bilinear')

        # 转换为图像格式
        cams_img_mean = (255 * cams_img_mean).to(torch.uint8)

        return cams_img_mean

    def forward(self, x):
        size = x.shape[2:]  # 输入图像的尺寸
        features = self.densenet121.features(x)  # Output from conv layers
        out = nn.functional.adaptive_avg_pool2d(features, (1, 1))  # Reduce to 1x1
        out = torch.flatten(out, 1)
        out = self.densenet121.classifier(out)
        cams = self.cam(self.feature, self.densenet121.classifier[0].weight, size)
        return out, cams

if __name__ == "__main__":
    main()